{
 "cells": [
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-26T10:15:41.825421Z",
     "start_time": "2025-01-26T10:15:38.459554Z"
    }
   },
   "source": [
    "import matplotlib as mpl\n",
    "import matplotlib.pyplot as plt\n",
    "%matplotlib inline\n",
    "import numpy as np\n",
    "import sklearn\n",
    "import pandas as pd\n",
    "import os\n",
    "import sys\n",
    "import time\n",
    "from tqdm.auto import tqdm\n",
    "import torch\n",
    "import torch.nn as nn\n",
    "import torch.nn.functional as F\n",
    "\n",
    "print(sys.version_info)\n",
    "for module in mpl, np, pd, sklearn, torch:\n",
    "    print(module.__name__, module.__version__)\n",
    "    \n",
    "device = torch.device(\"cuda:0\") if torch.cuda.is_available() else torch.device(\"cpu\")\n",
    "print(device)\n"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "sys.version_info(major=3, minor=12, micro=3, releaselevel='final', serial=0)\n",
      "matplotlib 3.10.0\n",
      "numpy 1.26.4\n",
      "pandas 2.2.3\n",
      "sklearn 1.6.1\n",
      "torch 2.5.1+cpu\n",
      "cpu\n"
     ]
    }
   ],
   "execution_count": 1
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 准备数据"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-26T10:15:44.753318Z",
     "start_time": "2025-01-26T10:15:41.826420Z"
    }
   },
   "source": [
    "from sklearn.datasets import fetch_california_housing\n",
    "\n",
    "housing = fetch_california_housing()\n",
    "print(housing.DESCR)\n",
    "print(housing.data.shape)\n",
    "print(housing.target.shape)"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      ".. _california_housing_dataset:\n",
      "\n",
      "California Housing dataset\n",
      "--------------------------\n",
      "\n",
      "**Data Set Characteristics:**\n",
      "\n",
      ":Number of Instances: 20640\n",
      "\n",
      ":Number of Attributes: 8 numeric, predictive attributes and the target\n",
      "\n",
      ":Attribute Information:\n",
      "    - MedInc        median income in block group\n",
      "    - HouseAge      median house age in block group\n",
      "    - AveRooms      average number of rooms per household\n",
      "    - AveBedrms     average number of bedrooms per household\n",
      "    - Population    block group population\n",
      "    - AveOccup      average number of household members\n",
      "    - Latitude      block group latitude\n",
      "    - Longitude     block group longitude\n",
      "\n",
      ":Missing Attribute Values: None\n",
      "\n",
      "This dataset was obtained from the StatLib repository.\n",
      "https://www.dcc.fc.up.pt/~ltorgo/Regression/cal_housing.html\n",
      "\n",
      "The target variable is the median house value for California districts,\n",
      "expressed in hundreds of thousands of dollars ($100,000).\n",
      "\n",
      "This dataset was derived from the 1990 U.S. census, using one row per census\n",
      "block group. A block group is the smallest geographical unit for which the U.S.\n",
      "Census Bureau publishes sample data (a block group typically has a population\n",
      "of 600 to 3,000 people).\n",
      "\n",
      "A household is a group of people residing within a home. Since the average\n",
      "number of rooms and bedrooms in this dataset are provided per household, these\n",
      "columns may take surprisingly large values for block groups with few households\n",
      "and many empty houses, such as vacation resorts.\n",
      "\n",
      "It can be downloaded/loaded using the\n",
      ":func:`sklearn.datasets.fetch_california_housing` function.\n",
      "\n",
      ".. rubric:: References\n",
      "\n",
      "- Pace, R. Kelley and Ronald Barry, Sparse Spatial Autoregressions,\n",
      "  Statistics and Probability Letters, 33 (1997) 291-297\n",
      "\n",
      "(20640, 8)\n",
      "(20640,)\n"
     ]
    }
   ],
   "execution_count": 2
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-26T10:15:44.758430Z",
     "start_time": "2025-01-26T10:15:44.754325Z"
    }
   },
   "source": [
    "# print(housing.data[0:5])\n",
    "import pprint  #打印的格式比较 好看\n",
    "\n",
    "pprint.pprint(housing.data[0:5])\n",
    "print('-'*50)\n",
    "pprint.pprint(housing.target[0:5])"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "array([[ 8.32520000e+00,  4.10000000e+01,  6.98412698e+00,\n",
      "         1.02380952e+00,  3.22000000e+02,  2.55555556e+00,\n",
      "         3.78800000e+01, -1.22230000e+02],\n",
      "       [ 8.30140000e+00,  2.10000000e+01,  6.23813708e+00,\n",
      "         9.71880492e-01,  2.40100000e+03,  2.10984183e+00,\n",
      "         3.78600000e+01, -1.22220000e+02],\n",
      "       [ 7.25740000e+00,  5.20000000e+01,  8.28813559e+00,\n",
      "         1.07344633e+00,  4.96000000e+02,  2.80225989e+00,\n",
      "         3.78500000e+01, -1.22240000e+02],\n",
      "       [ 5.64310000e+00,  5.20000000e+01,  5.81735160e+00,\n",
      "         1.07305936e+00,  5.58000000e+02,  2.54794521e+00,\n",
      "         3.78500000e+01, -1.22250000e+02],\n",
      "       [ 3.84620000e+00,  5.20000000e+01,  6.28185328e+00,\n",
      "         1.08108108e+00,  5.65000000e+02,  2.18146718e+00,\n",
      "         3.78500000e+01, -1.22250000e+02]])\n",
      "--------------------------------------------------\n",
      "array([4.526, 3.585, 3.521, 3.413, 3.422])\n"
     ]
    }
   ],
   "execution_count": 3
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-26T10:15:44.803360Z",
     "start_time": "2025-01-26T10:15:44.759430Z"
    }
   },
   "source": [
    "from sklearn.model_selection import train_test_split\n",
    "\n",
    "#拆分训练集和测试集，random_state是随机种子,同样的随机数种子，是为了得到同样的随机值\n",
    "x_train_all, x_test, y_train_all, y_test = train_test_split(\n",
    "    housing.data, housing.target, random_state = 7)\n",
    "x_train, x_valid, y_train, y_valid = train_test_split(\n",
    "    x_train_all, y_train_all, random_state = 11)\n",
    "# 训练集\n",
    "print(x_train.shape, y_train.shape)\n",
    "# 验证集\n",
    "print(x_valid.shape, y_valid.shape)\n",
    "# 测试集\n",
    "print(x_test.shape, y_test.shape)\n",
    "\n",
    "dataset_maps = {\n",
    "    \"train\": [x_train, y_train],\n",
    "    \"valid\": [x_valid, y_valid],\n",
    "    \"test\": [x_test, y_test],\n",
    "}\n"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "(11610, 8) (11610,)\n",
      "(3870, 8) (3870,)\n",
      "(5160, 8) (5160,)\n"
     ]
    }
   ],
   "execution_count": 4
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-26T10:15:44.812819Z",
     "start_time": "2025-01-26T10:15:44.804351Z"
    }
   },
   "source": [
    "from sklearn.preprocessing import StandardScaler\n",
    "from torch.utils.data import DataLoader\n",
    "\n",
    "\n",
    "scaler = StandardScaler()\n",
    "scaler.fit(x_train)"
   ],
   "outputs": [
    {
     "data": {
      "text/plain": [
       "StandardScaler()"
      ],
      "text/html": [
       "<style>#sk-container-id-1 {\n",
       "  /* Definition of color scheme common for light and dark mode */\n",
       "  --sklearn-color-text: #000;\n",
       "  --sklearn-color-text-muted: #666;\n",
       "  --sklearn-color-line: gray;\n",
       "  /* Definition of color scheme for unfitted estimators */\n",
       "  --sklearn-color-unfitted-level-0: #fff5e6;\n",
       "  --sklearn-color-unfitted-level-1: #f6e4d2;\n",
       "  --sklearn-color-unfitted-level-2: #ffe0b3;\n",
       "  --sklearn-color-unfitted-level-3: chocolate;\n",
       "  /* Definition of color scheme for fitted estimators */\n",
       "  --sklearn-color-fitted-level-0: #f0f8ff;\n",
       "  --sklearn-color-fitted-level-1: #d4ebff;\n",
       "  --sklearn-color-fitted-level-2: #b3dbfd;\n",
       "  --sklearn-color-fitted-level-3: cornflowerblue;\n",
       "\n",
       "  /* Specific color for light theme */\n",
       "  --sklearn-color-text-on-default-background: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, black)));\n",
       "  --sklearn-color-background: var(--sg-background-color, var(--theme-background, var(--jp-layout-color0, white)));\n",
       "  --sklearn-color-border-box: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, black)));\n",
       "  --sklearn-color-icon: #696969;\n",
       "\n",
       "  @media (prefers-color-scheme: dark) {\n",
       "    /* Redefinition of color scheme for dark theme */\n",
       "    --sklearn-color-text-on-default-background: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, white)));\n",
       "    --sklearn-color-background: var(--sg-background-color, var(--theme-background, var(--jp-layout-color0, #111)));\n",
       "    --sklearn-color-border-box: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, white)));\n",
       "    --sklearn-color-icon: #878787;\n",
       "  }\n",
       "}\n",
       "\n",
       "#sk-container-id-1 {\n",
       "  color: var(--sklearn-color-text);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 pre {\n",
       "  padding: 0;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 input.sk-hidden--visually {\n",
       "  border: 0;\n",
       "  clip: rect(1px 1px 1px 1px);\n",
       "  clip: rect(1px, 1px, 1px, 1px);\n",
       "  height: 1px;\n",
       "  margin: -1px;\n",
       "  overflow: hidden;\n",
       "  padding: 0;\n",
       "  position: absolute;\n",
       "  width: 1px;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-dashed-wrapped {\n",
       "  border: 1px dashed var(--sklearn-color-line);\n",
       "  margin: 0 0.4em 0.5em 0.4em;\n",
       "  box-sizing: border-box;\n",
       "  padding-bottom: 0.4em;\n",
       "  background-color: var(--sklearn-color-background);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-container {\n",
       "  /* jupyter's `normalize.less` sets `[hidden] { display: none; }`\n",
       "     but bootstrap.min.css set `[hidden] { display: none !important; }`\n",
       "     so we also need the `!important` here to be able to override the\n",
       "     default hidden behavior on the sphinx rendered scikit-learn.org.\n",
       "     See: https://github.com/scikit-learn/scikit-learn/issues/21755 */\n",
       "  display: inline-block !important;\n",
       "  position: relative;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-text-repr-fallback {\n",
       "  display: none;\n",
       "}\n",
       "\n",
       "div.sk-parallel-item,\n",
       "div.sk-serial,\n",
       "div.sk-item {\n",
       "  /* draw centered vertical line to link estimators */\n",
       "  background-image: linear-gradient(var(--sklearn-color-text-on-default-background), var(--sklearn-color-text-on-default-background));\n",
       "  background-size: 2px 100%;\n",
       "  background-repeat: no-repeat;\n",
       "  background-position: center center;\n",
       "}\n",
       "\n",
       "/* Parallel-specific style estimator block */\n",
       "\n",
       "#sk-container-id-1 div.sk-parallel-item::after {\n",
       "  content: \"\";\n",
       "  width: 100%;\n",
       "  border-bottom: 2px solid var(--sklearn-color-text-on-default-background);\n",
       "  flex-grow: 1;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-parallel {\n",
       "  display: flex;\n",
       "  align-items: stretch;\n",
       "  justify-content: center;\n",
       "  background-color: var(--sklearn-color-background);\n",
       "  position: relative;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-parallel-item {\n",
       "  display: flex;\n",
       "  flex-direction: column;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-parallel-item:first-child::after {\n",
       "  align-self: flex-end;\n",
       "  width: 50%;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-parallel-item:last-child::after {\n",
       "  align-self: flex-start;\n",
       "  width: 50%;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-parallel-item:only-child::after {\n",
       "  width: 0;\n",
       "}\n",
       "\n",
       "/* Serial-specific style estimator block */\n",
       "\n",
       "#sk-container-id-1 div.sk-serial {\n",
       "  display: flex;\n",
       "  flex-direction: column;\n",
       "  align-items: center;\n",
       "  background-color: var(--sklearn-color-background);\n",
       "  padding-right: 1em;\n",
       "  padding-left: 1em;\n",
       "}\n",
       "\n",
       "\n",
       "/* Toggleable style: style used for estimator/Pipeline/ColumnTransformer box that is\n",
       "clickable and can be expanded/collapsed.\n",
       "- Pipeline and ColumnTransformer use this feature and define the default style\n",
       "- Estimators will overwrite some part of the style using the `sk-estimator` class\n",
       "*/\n",
       "\n",
       "/* Pipeline and ColumnTransformer style (default) */\n",
       "\n",
       "#sk-container-id-1 div.sk-toggleable {\n",
       "  /* Default theme specific background. It is overwritten whether we have a\n",
       "  specific estimator or a Pipeline/ColumnTransformer */\n",
       "  background-color: var(--sklearn-color-background);\n",
       "}\n",
       "\n",
       "/* Toggleable label */\n",
       "#sk-container-id-1 label.sk-toggleable__label {\n",
       "  cursor: pointer;\n",
       "  display: flex;\n",
       "  width: 100%;\n",
       "  margin-bottom: 0;\n",
       "  padding: 0.5em;\n",
       "  box-sizing: border-box;\n",
       "  text-align: center;\n",
       "  align-items: start;\n",
       "  justify-content: space-between;\n",
       "  gap: 0.5em;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 label.sk-toggleable__label .caption {\n",
       "  font-size: 0.6rem;\n",
       "  font-weight: lighter;\n",
       "  color: var(--sklearn-color-text-muted);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 label.sk-toggleable__label-arrow:before {\n",
       "  /* Arrow on the left of the label */\n",
       "  content: \"▸\";\n",
       "  float: left;\n",
       "  margin-right: 0.25em;\n",
       "  color: var(--sklearn-color-icon);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 label.sk-toggleable__label-arrow:hover:before {\n",
       "  color: var(--sklearn-color-text);\n",
       "}\n",
       "\n",
       "/* Toggleable content - dropdown */\n",
       "\n",
       "#sk-container-id-1 div.sk-toggleable__content {\n",
       "  max-height: 0;\n",
       "  max-width: 0;\n",
       "  overflow: hidden;\n",
       "  text-align: left;\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-0);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-toggleable__content.fitted {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-0);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-toggleable__content pre {\n",
       "  margin: 0.2em;\n",
       "  border-radius: 0.25em;\n",
       "  color: var(--sklearn-color-text);\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-0);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-toggleable__content.fitted pre {\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-0);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 input.sk-toggleable__control:checked~div.sk-toggleable__content {\n",
       "  /* Expand drop-down */\n",
       "  max-height: 200px;\n",
       "  max-width: 100%;\n",
       "  overflow: auto;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 input.sk-toggleable__control:checked~label.sk-toggleable__label-arrow:before {\n",
       "  content: \"▾\";\n",
       "}\n",
       "\n",
       "/* Pipeline/ColumnTransformer-specific style */\n",
       "\n",
       "#sk-container-id-1 div.sk-label input.sk-toggleable__control:checked~label.sk-toggleable__label {\n",
       "  color: var(--sklearn-color-text);\n",
       "  background-color: var(--sklearn-color-unfitted-level-2);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-label.fitted input.sk-toggleable__control:checked~label.sk-toggleable__label {\n",
       "  background-color: var(--sklearn-color-fitted-level-2);\n",
       "}\n",
       "\n",
       "/* Estimator-specific style */\n",
       "\n",
       "/* Colorize estimator box */\n",
       "#sk-container-id-1 div.sk-estimator input.sk-toggleable__control:checked~label.sk-toggleable__label {\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-2);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-estimator.fitted input.sk-toggleable__control:checked~label.sk-toggleable__label {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-2);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-label label.sk-toggleable__label,\n",
       "#sk-container-id-1 div.sk-label label {\n",
       "  /* The background is the default theme color */\n",
       "  color: var(--sklearn-color-text-on-default-background);\n",
       "}\n",
       "\n",
       "/* On hover, darken the color of the background */\n",
       "#sk-container-id-1 div.sk-label:hover label.sk-toggleable__label {\n",
       "  color: var(--sklearn-color-text);\n",
       "  background-color: var(--sklearn-color-unfitted-level-2);\n",
       "}\n",
       "\n",
       "/* Label box, darken color on hover, fitted */\n",
       "#sk-container-id-1 div.sk-label.fitted:hover label.sk-toggleable__label.fitted {\n",
       "  color: var(--sklearn-color-text);\n",
       "  background-color: var(--sklearn-color-fitted-level-2);\n",
       "}\n",
       "\n",
       "/* Estimator label */\n",
       "\n",
       "#sk-container-id-1 div.sk-label label {\n",
       "  font-family: monospace;\n",
       "  font-weight: bold;\n",
       "  display: inline-block;\n",
       "  line-height: 1.2em;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-label-container {\n",
       "  text-align: center;\n",
       "}\n",
       "\n",
       "/* Estimator-specific */\n",
       "#sk-container-id-1 div.sk-estimator {\n",
       "  font-family: monospace;\n",
       "  border: 1px dotted var(--sklearn-color-border-box);\n",
       "  border-radius: 0.25em;\n",
       "  box-sizing: border-box;\n",
       "  margin-bottom: 0.5em;\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-0);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-estimator.fitted {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-0);\n",
       "}\n",
       "\n",
       "/* on hover */\n",
       "#sk-container-id-1 div.sk-estimator:hover {\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-2);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-estimator.fitted:hover {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-2);\n",
       "}\n",
       "\n",
       "/* Specification for estimator info (e.g. \"i\" and \"?\") */\n",
       "\n",
       "/* Common style for \"i\" and \"?\" */\n",
       "\n",
       ".sk-estimator-doc-link,\n",
       "a:link.sk-estimator-doc-link,\n",
       "a:visited.sk-estimator-doc-link {\n",
       "  float: right;\n",
       "  font-size: smaller;\n",
       "  line-height: 1em;\n",
       "  font-family: monospace;\n",
       "  background-color: var(--sklearn-color-background);\n",
       "  border-radius: 1em;\n",
       "  height: 1em;\n",
       "  width: 1em;\n",
       "  text-decoration: none !important;\n",
       "  margin-left: 0.5em;\n",
       "  text-align: center;\n",
       "  /* unfitted */\n",
       "  border: var(--sklearn-color-unfitted-level-1) 1pt solid;\n",
       "  color: var(--sklearn-color-unfitted-level-1);\n",
       "}\n",
       "\n",
       ".sk-estimator-doc-link.fitted,\n",
       "a:link.sk-estimator-doc-link.fitted,\n",
       "a:visited.sk-estimator-doc-link.fitted {\n",
       "  /* fitted */\n",
       "  border: var(--sklearn-color-fitted-level-1) 1pt solid;\n",
       "  color: var(--sklearn-color-fitted-level-1);\n",
       "}\n",
       "\n",
       "/* On hover */\n",
       "div.sk-estimator:hover .sk-estimator-doc-link:hover,\n",
       ".sk-estimator-doc-link:hover,\n",
       "div.sk-label-container:hover .sk-estimator-doc-link:hover,\n",
       ".sk-estimator-doc-link:hover {\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-3);\n",
       "  color: var(--sklearn-color-background);\n",
       "  text-decoration: none;\n",
       "}\n",
       "\n",
       "div.sk-estimator.fitted:hover .sk-estimator-doc-link.fitted:hover,\n",
       ".sk-estimator-doc-link.fitted:hover,\n",
       "div.sk-label-container:hover .sk-estimator-doc-link.fitted:hover,\n",
       ".sk-estimator-doc-link.fitted:hover {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-3);\n",
       "  color: var(--sklearn-color-background);\n",
       "  text-decoration: none;\n",
       "}\n",
       "\n",
       "/* Span, style for the box shown on hovering the info icon */\n",
       ".sk-estimator-doc-link span {\n",
       "  display: none;\n",
       "  z-index: 9999;\n",
       "  position: relative;\n",
       "  font-weight: normal;\n",
       "  right: .2ex;\n",
       "  padding: .5ex;\n",
       "  margin: .5ex;\n",
       "  width: min-content;\n",
       "  min-width: 20ex;\n",
       "  max-width: 50ex;\n",
       "  color: var(--sklearn-color-text);\n",
       "  box-shadow: 2pt 2pt 4pt #999;\n",
       "  /* unfitted */\n",
       "  background: var(--sklearn-color-unfitted-level-0);\n",
       "  border: .5pt solid var(--sklearn-color-unfitted-level-3);\n",
       "}\n",
       "\n",
       ".sk-estimator-doc-link.fitted span {\n",
       "  /* fitted */\n",
       "  background: var(--sklearn-color-fitted-level-0);\n",
       "  border: var(--sklearn-color-fitted-level-3);\n",
       "}\n",
       "\n",
       ".sk-estimator-doc-link:hover span {\n",
       "  display: block;\n",
       "}\n",
       "\n",
       "/* \"?\"-specific style due to the `<a>` HTML tag */\n",
       "\n",
       "#sk-container-id-1 a.estimator_doc_link {\n",
       "  float: right;\n",
       "  font-size: 1rem;\n",
       "  line-height: 1em;\n",
       "  font-family: monospace;\n",
       "  background-color: var(--sklearn-color-background);\n",
       "  border-radius: 1rem;\n",
       "  height: 1rem;\n",
       "  width: 1rem;\n",
       "  text-decoration: none;\n",
       "  /* unfitted */\n",
       "  color: var(--sklearn-color-unfitted-level-1);\n",
       "  border: var(--sklearn-color-unfitted-level-1) 1pt solid;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 a.estimator_doc_link.fitted {\n",
       "  /* fitted */\n",
       "  border: var(--sklearn-color-fitted-level-1) 1pt solid;\n",
       "  color: var(--sklearn-color-fitted-level-1);\n",
       "}\n",
       "\n",
       "/* On hover */\n",
       "#sk-container-id-1 a.estimator_doc_link:hover {\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-3);\n",
       "  color: var(--sklearn-color-background);\n",
       "  text-decoration: none;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 a.estimator_doc_link.fitted:hover {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-3);\n",
       "}\n",
       "</style><div id=\"sk-container-id-1\" class=\"sk-top-container\"><div class=\"sk-text-repr-fallback\"><pre>StandardScaler()</pre><b>In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. <br />On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.</b></div><div class=\"sk-container\" hidden><div class=\"sk-item\"><div class=\"sk-estimator fitted sk-toggleable\"><input class=\"sk-toggleable__control sk-hidden--visually\" id=\"sk-estimator-id-1\" type=\"checkbox\" checked><label for=\"sk-estimator-id-1\" class=\"sk-toggleable__label fitted sk-toggleable__label-arrow\"><div><div>StandardScaler</div></div><div><a class=\"sk-estimator-doc-link fitted\" rel=\"noreferrer\" target=\"_blank\" href=\"https://scikit-learn.org/1.6/modules/generated/sklearn.preprocessing.StandardScaler.html\">?<span>Documentation for StandardScaler</span></a><span class=\"sk-estimator-doc-link fitted\">i<span>Fitted</span></span></div></label><div class=\"sk-toggleable__content fitted\"><pre>StandardScaler()</pre></div> </div></div></div></div>"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 5
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 构建数据集\n",
    "\n",
    "这里我们构建多输入的数据集，注意到数据集介绍里对每个特征定义如下：\n",
    "\n",
    "> **Attribute Information**:\n",
    "> - MedInc        median income in block group\n",
    "> - HouseAge      median house age in block group\n",
    "> - AveRooms      average number of rooms per household\n",
    "> - AveBedrms     average number of bedrooms per household\n",
    "> - Population    block group population\n",
    "> - AveOccup      average number of household members\n",
    "> - Latitude      block group latitude\n",
    "> - Longitude     block group longitude\n",
    "\n",
    "我们认为最后两维作为位置信息要单独处理，故制作数据集如下"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-26T10:15:44.821687Z",
     "start_time": "2025-01-26T10:15:44.813816Z"
    }
   },
   "source": [
    "from torch.utils.data import Dataset\n",
    "\n",
    "class HousingDataset(Dataset):\n",
    "    def __init__(self, mode='train'):\n",
    "        self.x, self.y = dataset_maps[mode]\n",
    "        self.x = torch.from_numpy(scaler.transform(self.x)).float()\n",
    "        self.y = torch.from_numpy(self.y).float().reshape(-1, 1)\n",
    "            \n",
    "    def __len__(self):\n",
    "        return len(self.x)\n",
    "    \n",
    "    def __getitem__(self, idx):\n",
    "        return (self.x[idx],self.x[idx][-2:]), self.y[idx] #返回的是一个元组，元组里面是两个元素，第一个元素是一个元组，第二个元素是一个数。self.x[idx][-2:]代表取最后两个元素\n",
    "    \n",
    "    \n",
    "train_ds = HousingDataset(\"train\")\n",
    "valid_ds = HousingDataset(\"valid\")\n",
    "test_ds = HousingDataset(\"test\")"
   ],
   "outputs": [],
   "execution_count": 6
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-26T10:15:44.831734Z",
     "start_time": "2025-01-26T10:15:44.821687Z"
    }
   },
   "source": [
    "train_ds[1]"
   ],
   "outputs": [
    {
     "data": {
      "text/plain": [
       "((tensor([-0.2981,  0.3523, -0.1092, -0.2506, -0.0341, -0.0060,  1.0806, -1.0611]),\n",
       "  tensor([ 1.0806, -1.0611])),\n",
       " tensor([1.5140]))"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 7
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### DataLoader"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-26T10:15:44.839318Z",
     "start_time": "2025-01-26T10:15:44.832769Z"
    }
   },
   "source": [
    "from torch.utils.data import DataLoader\n",
    "\n",
    "\n",
    "batch_size = 8\n",
    "train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=False)\n",
    "val_loader = DataLoader(valid_ds, batch_size=batch_size, shuffle=False)\n",
    "test_loader = DataLoader(test_ds, batch_size=batch_size, shuffle=False)"
   ],
   "outputs": [],
   "execution_count": 8
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 定义模型"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-26T10:15:44.851750Z",
     "start_time": "2025-01-26T10:15:44.839904Z"
    }
   },
   "source": [
    "#回归模型我们只需要1个数\n",
    "\n",
    "class WideDeep(nn.Module):\n",
    "    def __init__(self, input_dim=(8, 2)):\n",
    "        super().__init__()\n",
    "        self.deep = nn.Sequential(\n",
    "            nn.Linear(input_dim[1], 30),\n",
    "            nn.ReLU(),\n",
    "            nn.Linear(30, 30),\n",
    "            nn.ReLU()\n",
    "            )\n",
    "        # pytorch 需要自行计算输出输出维度\n",
    "        self.output_layer = nn.Linear(30 + input_dim[0], 1)\n",
    "        \n",
    "        # 初始化权重\n",
    "        self.init_weights()\n",
    "        \n",
    "    def init_weights(self):\n",
    "        \"\"\"使用 xavier 均匀分布来初始化全连接层的权重 W\"\"\"\n",
    "        for m in self.modules():\n",
    "            if isinstance(m, nn.Linear):\n",
    "                nn.init.xavier_uniform_(m.weight)\n",
    "                nn.init.zeros_(m.bias)\n",
    "        \n",
    "    def forward(self, x_wide, x_deep):\n",
    "        # x_deep.shape [batch size, 6]\n",
    "        deep_output = self.deep(x_deep)\n",
    "        # concat [batch size, 30] with [batch size 8]\n",
    "        concat = torch.cat([x_wide, deep_output], dim=1)\n",
    "        logits = self.output_layer(concat)\n",
    "        # logits.shape [batch size, 1]\n",
    "        return logits"
   ],
   "outputs": [],
   "execution_count": 9
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-26T10:15:44.862330Z",
     "start_time": "2025-01-26T10:15:44.852791Z"
    }
   },
   "source": [
    "class EarlyStopCallback:\n",
    "    def __init__(self, patience=5, min_delta=0.01):\n",
    "        \"\"\"\n",
    "\n",
    "        Args:\n",
    "            patience (int, optional): Number of epochs with no improvement after which training will be stopped.. Defaults to 5.\n",
    "            min_delta (float, optional): Minimum change in the monitored quantity to qualify as an improvement, i.e. an absolute \n",
    "                change of less than min_delta, will count as no improvement. Defaults to 0.01.\n",
    "        \"\"\"\n",
    "        self.patience = patience\n",
    "        self.min_delta = min_delta\n",
    "        self.best_metric = -1\n",
    "        self.counter = 0\n",
    "        \n",
    "    def __call__(self, metric):\n",
    "        if metric >= self.best_metric + self.min_delta:\n",
    "            # update best metric\n",
    "            self.best_metric = metric\n",
    "            # reset counter \n",
    "            self.counter = 0\n",
    "        else: \n",
    "            self.counter += 1\n",
    "            \n",
    "    @property\n",
    "    def early_stop(self):\n",
    "        return self.counter >= self.patience\n"
   ],
   "outputs": [],
   "execution_count": 10
  },
  {
   "cell_type": "code",
   "source": [
    "model = WideDeep()\n",
    "model"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2025-01-26T10:15:44.872852Z",
     "start_time": "2025-01-26T10:15:44.863322Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "WideDeep(\n",
       "  (deep): Sequential(\n",
       "    (0): Linear(in_features=2, out_features=30, bias=True)\n",
       "    (1): ReLU()\n",
       "    (2): Linear(in_features=30, out_features=30, bias=True)\n",
       "    (3): ReLU()\n",
       "  )\n",
       "  (output_layer): Linear(in_features=38, out_features=1, bias=True)\n",
       ")"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 11
  },
  {
   "cell_type": "code",
   "source": [
    "for name, param in model.named_parameters():\n",
    "      print(name, param.shape)"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2025-01-26T10:15:44.882241Z",
     "start_time": "2025-01-26T10:15:44.873839Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "deep.0.weight torch.Size([30, 2])\n",
      "deep.0.bias torch.Size([30])\n",
      "deep.2.weight torch.Size([30, 30])\n",
      "deep.2.bias torch.Size([30])\n",
      "output_layer.weight torch.Size([1, 38])\n",
      "output_layer.bias torch.Size([1])\n"
     ]
    }
   ],
   "execution_count": 12
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-26T10:15:44.891122Z",
     "start_time": "2025-01-26T10:15:44.882741Z"
    }
   },
   "source": [
    "from sklearn.metrics import accuracy_score\n",
    "\n",
    "@torch.no_grad()\n",
    "def evaluating(model, dataloader, loss_fct):\n",
    "    loss_list = []\n",
    "    for (datas_deep, datas_wide), labels in dataloader:\n",
    "        datas_deep = datas_deep.to(device)\n",
    "        datas_wide = datas_wide.to(device)\n",
    "        labels = labels.to(device)\n",
    "        # 前向计算\n",
    "        logits = model(datas_deep, datas_wide)\n",
    "        loss = loss_fct(logits, labels)         # 验证集损失\n",
    "        loss_list.append(loss.item())\n",
    "        \n",
    "    return np.mean(loss_list)\n"
   ],
   "outputs": [],
   "execution_count": 13
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-26T10:16:03.310478Z",
     "start_time": "2025-01-26T10:15:44.892864Z"
    }
   },
   "source": [
    "# 训练\n",
    "def training(\n",
    "    model, \n",
    "    train_loader, \n",
    "    val_loader, \n",
    "    epoch, \n",
    "    loss_fct, \n",
    "    optimizer, \n",
    "    tensorboard_callback=None,\n",
    "    save_ckpt_callback=None,\n",
    "    early_stop_callback=None,\n",
    "    eval_step=500,\n",
    "    ):\n",
    "    record_dict = {\n",
    "        \"train\": [],\n",
    "        \"val\": []\n",
    "    }\n",
    "    \n",
    "    global_step = 0\n",
    "    model.train()\n",
    "    with tqdm(total=epoch * len(train_loader)) as pbar:\n",
    "        for epoch_id in range(epoch):\n",
    "            # training\n",
    "            for (datas_deep, datas_wide), labels in train_loader:\n",
    "                datas_deep = datas_deep.to(device)\n",
    "                datas_wide = datas_wide.to(device)\n",
    "                labels = labels.to(device)\n",
    "                # 梯度清空\n",
    "                optimizer.zero_grad()\n",
    "                # 模型前向计算\n",
    "                logits = model(datas_deep, datas_wide)\n",
    "                # 计算损失\n",
    "                loss = loss_fct(logits, labels)\n",
    "                # 梯度回传\n",
    "                loss.backward()\n",
    "                # 调整优化器，包括学习率的变动等\n",
    "                optimizer.step()\n",
    " \n",
    "                loss = loss.cpu().item()\n",
    "                # record\n",
    "                \n",
    "                record_dict[\"train\"].append({\n",
    "                    \"loss\": loss, \"step\": global_step\n",
    "                })\n",
    "                \n",
    "                # evaluating\n",
    "                if global_step % eval_step == 0:\n",
    "                    model.eval()\n",
    "                    val_loss = evaluating(model, val_loader, loss_fct)\n",
    "                    record_dict[\"val\"].append({\n",
    "                        \"loss\": val_loss, \"step\": global_step\n",
    "                    })\n",
    "                    model.train()\n",
    "\n",
    "                    # 早停 Early Stop\n",
    "                    if early_stop_callback is not None:\n",
    "                        early_stop_callback(-val_loss)\n",
    "                        if early_stop_callback.early_stop:\n",
    "                            print(f\"Early stop at epoch {epoch_id} / global_step {global_step}\")\n",
    "                            return record_dict\n",
    "                    \n",
    "                # udate step\n",
    "                global_step += 1\n",
    "                pbar.update(1)\n",
    "                pbar.set_postfix({\"epoch\": epoch_id})\n",
    "        \n",
    "    return record_dict\n",
    "        \n",
    "\n",
    "epoch = 10\n",
    "\n",
    "\n",
    "\n",
    "# 1. 定义损失函数 采用MSE损失\n",
    "loss_fct = nn.MSELoss()\n",
    "# 2. 定义优化器 采用SGD\n",
    "# Optimizers specified in the torch.optim package\n",
    "optimizer = torch.optim.SGD(model.parameters(), lr=0.001, momentum=0.0)\n",
    "\n",
    "# 3. early stop\n",
    "early_stop_callback = EarlyStopCallback(patience=5, min_delta=1e-3)\n",
    "\n",
    "model = model.to(device)\n",
    "record = training(\n",
    "    model, \n",
    "    train_loader, \n",
    "    val_loader, \n",
    "    epoch, \n",
    "    loss_fct, \n",
    "    optimizer, \n",
    "    early_stop_callback=early_stop_callback,\n",
    "    eval_step=len(train_loader)\n",
    "    )"
   ],
   "outputs": [
    {
     "data": {
      "text/plain": [
       "  0%|          | 0/14520 [00:00<?, ?it/s]"
      ],
      "application/vnd.jupyter.widget-view+json": {
       "version_major": 2,
       "version_minor": 0,
       "model_id": "21bb41a8defb423482fdd022c5295d12"
      }
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "IOPub message rate exceeded.\n",
      "The Jupyter server will temporarily stop sending output\n",
      "to the client in order to avoid crashing it.\n",
      "To change this limit, set the config variable\n",
      "`--ServerApp.iopub_msg_rate_limit`.\n",
      "\n",
      "Current values:\n",
      "ServerApp.iopub_msg_rate_limit=1000.0 (msgs/sec)\n",
      "ServerApp.rate_limit_window=3.0 (secs)\n",
      "\n",
      "IOPub message rate exceeded.\n",
      "The Jupyter server will temporarily stop sending output\n",
      "to the client in order to avoid crashing it.\n",
      "To change this limit, set the config variable\n",
      "`--ServerApp.iopub_msg_rate_limit`.\n",
      "\n",
      "Current values:\n",
      "ServerApp.iopub_msg_rate_limit=1000.0 (msgs/sec)\n",
      "ServerApp.rate_limit_window=3.0 (secs)\n",
      "\n",
      "IOPub message rate exceeded.\n",
      "The Jupyter server will temporarily stop sending output\n",
      "to the client in order to avoid crashing it.\n",
      "To change this limit, set the config variable\n",
      "`--ServerApp.iopub_msg_rate_limit`.\n",
      "\n",
      "Current values:\n",
      "ServerApp.iopub_msg_rate_limit=1000.0 (msgs/sec)\n",
      "ServerApp.rate_limit_window=3.0 (secs)\n",
      "\n",
      "IOPub message rate exceeded.\n",
      "The Jupyter server will temporarily stop sending output\n",
      "to the client in order to avoid crashing it.\n",
      "To change this limit, set the config variable\n",
      "`--ServerApp.iopub_msg_rate_limit`.\n",
      "\n",
      "Current values:\n",
      "ServerApp.iopub_msg_rate_limit=1000.0 (msgs/sec)\n",
      "ServerApp.rate_limit_window=3.0 (secs)\n",
      "\n",
      "IOPub message rate exceeded.\n",
      "The Jupyter server will temporarily stop sending output\n",
      "to the client in order to avoid crashing it.\n",
      "To change this limit, set the config variable\n",
      "`--ServerApp.iopub_msg_rate_limit`.\n",
      "\n",
      "Current values:\n",
      "ServerApp.iopub_msg_rate_limit=1000.0 (msgs/sec)\n",
      "ServerApp.rate_limit_window=3.0 (secs)\n",
      "\n"
     ]
    }
   ],
   "execution_count": 14
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-26T10:18:38.066573Z",
     "start_time": "2025-01-26T10:18:37.969509Z"
    }
   },
   "source": [
    "#画线要注意的是损失是不一定在零到1之间的\n",
    "def plot_learning_curves(record_dict, sample_step=500):\n",
    "    # build DataFrame\n",
    "    train_df = pd.DataFrame(record_dict[\"train\"]).set_index(\"step\").iloc[::sample_step]\n",
    "    val_df = pd.DataFrame(record_dict[\"val\"]).set_index(\"step\")\n",
    "\n",
    "    # plot\n",
    "    for idx, item in enumerate(train_df.columns):\n",
    "        plt.plot(train_df.index, train_df[item], label=f\"train_{item}\")\n",
    "        plt.plot(val_df.index, val_df[item], label=f\"val_{item}\")\n",
    "        plt.grid()\n",
    "        plt.legend()\n",
    "        # plt.xticks(range(0, train_df.index[-1], 10*sample_step), range(0, train_df.index[-1], 10*sample_step))\n",
    "        plt.xlabel(\"step\")\n",
    "\n",
    "        plt.show()\n",
    "\n",
    "plot_learning_curves(record, sample_step=500)  #横坐标是 steps"
   ],
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ],
      "image/png": "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UpmSr776St0nWRw9SAkUlqr5EK6eF454ISLyF+kekK4PX5wkmCjJ9q6uCW9IpkHqAyXAOHToEBYvXqwCEiGrzkqm49VXX1W3S9dGq1atVBeIGT744AP89ddf6NatG7799ls0btwYy5cvV/dJULRlyxaVGZk/fz6aN2+u9jWQWDb4SMnkiBciIipaZGQkbrrpJpXxkNqKJk2aoH379uo+OfDfc889uPHGG1XQUb16dezfv98nz9usWTNVNCq1Hzp5Psm4yD7opK5j9OjRWLp0KVq2bKm6a3QSjDz55JP4/fffVRs+//xzBBLLBh8cbktERMWRTIdkPj777DN31kOvo5gxY4bKeEigIN0gvlrqftiwYSrwGT58ODZv3qyKXqX49a677lKja/bt24exY8di2bJlaoSLBBi7du1SQYt0/UjNyYIFC9R9ErRI0apnTUggsFTNh2fwITUfREREFyPDXStVqqRqLSTA0L3zzjtqyK10e0gR6rPPPquKQH0hOjoav/32Gx5//HE1hFeu33zzzeo59fsl2JBajjNnzqhajxEjRuChhx5StSdy2913340TJ06ofZPMhwQrgcR6wYddxnhLwSlrPoiI6OKkq+Po0aOFTp0u9RSeJADwVJJuGC3f/CPSlZP/8XWS/fj666/VHB75C23Dw8NVF1GgK3G3y6JFi1QRjky+kn8u+uzsbBX9yYsmVbqyjURfhb1x/sJuFyIioiALPqQARoYASaVtfmlpaVi7di1eeOEFdS79YZKquv766xF4BafMfBARkfGkYFVmQS3s1KJFC1hRibtdZFyxnApTvnx5zJ07N89tMjFK586d1djnwmZ9y8zMVCed3mcmWRQ5+ZI8nh58JKdn+fzxA5XeTqu0V8d2W6vdVm57ILdb9km6FKQY01cFmfm7KvTHD1TXXXed+kFemLCwsBLvuz/bLc8nzyvvq0x25qkknz/Daz4SExNV94zMPV+Y8ePHF1oII9W7UlTjaxGu/rEtO3ZjTtZOWEn+wNAq2G7rsWrbA7HdoaGhahiqTMpl1BwYycnJCHRVq1Yt8r6kUhaq+qPd8h7KiBopwZDi1vy9HwERfMj0sFIDcscdd6jCmMLIGGWZjc3zTUhISEC/fv2K/D+lJVHZ7587F/6pWisB115rjXSXtFu+lGQmvECb399IbLe12m3ltgdyuyWzLZlv+THp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"
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "execution_count": 15
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 测试集"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-26T10:18:39.578146Z",
     "start_time": "2025-01-26T10:18:39.470988Z"
    }
   },
   "source": [
    "model.eval()\n",
    "loss = evaluating(model, val_loader, loss_fct)\n",
    "print(f\"loss:     {loss:.4f}\")"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "loss:     0.5191\n"
     ]
    }
   ],
   "execution_count": 16
  },
  {
   "metadata": {},
   "cell_type": "code",
   "outputs": [],
   "execution_count": null,
   "source": ""
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "pytorch",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.8"
  },
  "orig_nbformat": 4
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
